## C++ 初始化列表构造函数详解
🧑💻 User
In this lecture, we’re going to learn about initializer list constructors, and these are constructorsthat are used to initialize what we call aggregates in C plus plus and aggregates are really some kindsof containers and they are special containers in C plus plus and this special kind of containers thatcan contain stuff.For example, you see that we have an outer container which contains some little boxes in here and itis a representation of an aggregate in C plus plus.It is basically something that is going to aggregate a lot of little things together, but it’s notevery container in C plus plus there is a nuanced definition of aggregates, but for now you can thinkof an aggregate as just a container of things.And from the containers we have seen so far, an array is an aggregate and a struct which only has publicmember variables is also an aggregate.So all these aggregates can use a unified syntax to initialize them.For example, we can use the curly braces syntax here to initialize and put data in our container here.And when the compiler sees your initializer here, it knows how to generate the tools it will use toput this data in your container here.So, for example, it might choose to put one in this little box.It might choose to put two in this little box.It might choose to put three in this little box.It might choose to put four in this little box.And whatever we have after this is going to be put in sequential boxes that we have in our box containerhere.This is what a list initializer constructor is going to do.It’s going to be used by the compiler to take the data from our initializer here and put that in ouraggregate compound type.And at this point in the course we really know about to aggregate compound types, we have seen thatstruct is one type of aggregate and an array is also another type of aggregate.So we’re going to see how we can use an initializer list constructor with a struct because an arrayis really a built in type.We can’t mess with that and set up another way to initialize it.So we’re going to do this only for the struct in this course to let you learn about initializer listconstructors.Okay.So here is a simple struct we’re going to use.It’s going to be a point struct.It is going to be modeling a point.Inside we have two member variables.We have double X and double y and they are going to be public because we are using a structure.Members of a struct are public by default here.So we can set up a syntax like this to try and initialize this little guy and we’re going to see thatthis syntax is going to work even if we didn’t do anything to make this work.And what the compiler is going to do behind the scenes, it’s going to generate an initializer listconstructor and it’s going to use it to take the data from our curly braces here and store that in ourpoint object.So it is basically to take the first parameter here and store that in X and it is going to take thesecond parameter and store that in Y here.But sometimes you need to change the behavior of this initializer list constructors and we’re goingto see how we can set up our own and let to do exactly what we want.And here is a simple example of how you can do that in initializer List constructor is going to takethis form here.It’s going to have the name of the constructor, which is the name of the class.But our parameter list is going to take an STD initializer list, which is going to be an instance ofa template class.We haven’t really learned about class templates, but please think about STD initializer list here asan instance of a class template and you see that we initialize this for double because we know thatour class is going to contain double member variables.So these two things have to match the type of your member variables, has to match the type that youpass in your STD initializer list.Instance.Once you have this initializer list parameter, the compiler is going to take what you call in yourmain function and use that to create an STD initializer list object.So what is going to basically happen is if we issue a call like this, the compiler is going to noticethat we are using an initializer list here and it is going to grab whatever we have here and it is goingto call our initializer List constructor.So it is going to create a parameter that it is going to pass to that constructor.And the parameter happens to be an STD initializer list, which is going to be an instance for doubletype.So this is what we’re going to have here.So the compiler is so the compiler is going to take the first parameter and put that in the first spotfor our initializer list here.And it’s going to take the second parameter.And put that in the second spot in our STD initializer list.Instance object here.Okay, so now that we have the data, we need to grab the data and use that in our object and we’regoing to use a concept we haven’t really learnt about to do this.We’re going to use what we call iterators, but we haven’t learnt about them because we don’t have enoughtools to understand them fully yet.But I am going to try and help you understand this.Think of the begin function We call on our initializer list here as a function that is going to giveyou the starting pointer in our collection.So Begin is basically going to be a pointer to the first element in our initializer list here, andend is going to be a pointer past the last element in our initializer list here.So let’s say that Begin is going to be a pointer to the start of our list.It’s going to be a pointer to the first element in the list and end is going to be a pointer past thelast element in our initializer list here.So it’s not going to point to 45.3 here.It’s going to point past the last element.So it’s going to point to the spot past the last element in our initializer list here.So hopefully you have an understanding of what begin and end here are.So we’re going to use this information to our advantage in this loop here.So we’re going to use this to our advantage in this for loop, we’re going to say for auto I, the beginningis going to be the beginning pointer and we’re going to keep looping as long as our pointer is not theend pointer and as long as we have a valid pointer, we’re going to keep incrementing.And if we increment for the first iteration, we’re going to grab the first element in our initializerlist.And for the second iteration we’re going to grab the second element.And once we hit the third iteration, we’re going to be basically at the end of our list and we willstop printing stuff here so we can use this syntax and print information that we have in our initializerlist.And notice that what we use inside our body is the dereferencing operator to dereference this pointerand get to what is pointed to by this iterator pointer here.Okay.So now that we have this, we can even use pointer arithmetic notation to grab data from our iterator.So for example, we can grab what is pointed to by the begin pointer using the syntax here and we cangrab the second element using the syntax here, using pointer arithmetic on our begin pointer here andwe can do this and we’re going to be using this to our advantage to grab data that we store in our membervariables here.So please take a look at our Initializer List constructor here and see what we are doing.We are grabbing data from our initializer list parameter and then the first thing is going to be usedto initialize the x member variable.And the second thing is going to be used to initialize our y member variable.And with this we have successfully grabbed the data from our initializer list and used that to initializeour member variables.This is how you can set this up and realize the begin thing can throw you off a little bit.But please think of this as a pointer to the beginning element in your initializer list collection.Once you understand this like this, you will be able to use your initializer list constructors withouta problem.Okay, Now that you know this, we’re going to head over to Visual Studio code and try this out.Okay.Here we are in our working folder.The current project is Initializer List Constructors.We’re going to grab our template files and we’re going to put them in place and we are going to openthis in Visual Studio code by dragging and dropping here, This is going to open our folder and we’regoing to close the sidebar here and clean up a little bit.We’re going to set up a struct which is going to be our sandbox.So let’s say that it is going to be a point struct and inside we’re going to have two member variables.We’re going to have a double variable for x, the x coordinate of our point and we’re going to havea double variable for our Y coordinate here and we’re going to set up a function which is going to printour point easily.So let’s put a semicolon after here and we’re going to say void print point and we’re going to takethe point by const reference and we’re going to print it out with Stdcout.We’re going to say Stdcout, we’re going to say point and we’re going to grab its X, coordinate andprint that out.We’re going to say p x here and we’re going to grab the Y coordinate and print that out as well.We’re going to say p y.Think we can save that and then we’re going to.Close this off with a square bracket and then we’re going to put our new line character.This is going to do.Now that we have this, we can set up a point in the main function and we’re going to do that like this.We’re going to say point P one and we are going to use a curly brace, aggregate initializer.So we’re going to use this syntax here and we’re going to put data in our point.We’re going to put in a few arguments, let’s say 22.4 and 65.4.We can really put in anything.We just want to see that the data we specify here is going to end up in the object.And after we set up the object, we’re going to print the information in here.So we’re going to say print point and we’re going to pass our P one point and see what happens here.Let’s bolt.We’re going to use JK to do this.You’re going to see the world is going to be good and we’re going to clear and run.Rooster You’re going to see that we have our point here.And notice that this syntax is working.Even if we didn’t really set up any constructor to make this work.So how is this working?Well, the compiler is generating an initializer list constructor behind your back, and that initializerlist constructor is taking the data here and using some magic to take the data and put that in thismember variables.And when you get to use your object, for example, by printing it out here, you’re going to see thatthe data here has ended up in this p one object.What the initializer list is going to do by default is to take the first thing here and use that toinitialize X like this.And then it’s going to take the second thing you specify here and use that to initialize y like this.Or I should say this is what the initializer list constructor that is generated by the compiler is goingto do by default.If we want to see this happening, for example, through a debugger or we want to change this defaultbehavior, for example, we want to use the first thing to initialize y and the second thing to initializeX.Suppose you want that for your application.You will need to set up your own initializer list constructor and we’re going to do that in our structhere.So the way we’re going to do that, we’re going to modify our structure a little bit and we’re goingto put in our member variables.For example, we can make this two guys private and let’s bring this to the left a little bit, andwe’re going to put in a public section.And please note that this is just a preference.If you want, you can leave them as public.But I like to keep my member variables private unless I have a compelling reason not to do so.So we’re going to put in our initializer list constructor.It’s going to be called Point like any other constructor, and then it’s going to take an initializerlist parameter.And this is going to be a template instance for double for the initializer list class template we havein the C plus plus standard library.So this is something that is built in and we just want to use this here.So we’re going to call this list and then we’re going to put a body which is going to be used to passthis list and grab the data and put that in our member variables here, let’s say list properly.Okay.So in this list, we’re going to do things step by step.What we want to do is to put a message out that this constructor was actually called.So we’re going to say initializer list called or constructor called and we’re going to put this out.Okay.So let’s see that this is actually the case.And now you see that because we have made our member variables private, we no longer have access tothem, but we can take this function and make it part of our class here because we can do that.So let’s grab this and we’re going to make it a public function in our point class.We’re going to modify it a little bit.We’re going to take out the parameters here because we don’t need this and we’re going to make it acast member function because it’s not going to be modifying our class here and we’re going to printX and Y directly, and this is going to do what we want.We’re going to get rid of this squiggly lines and what we need to do to print our object.We don’t need to call this guy directly like this.We can say p one and print info or print point and it is going to print our point here.But it doesn’t make to call this function yet because we haven’t really put in any data.So let’s just make it a member for now and we’re going to use it later.Let’s take this call out and we want to see if our Initializer List constructor was called.That’s what we want to see here.So let’s build our program.But before we do that, we’re going to clean up a little bit because now it’s ugly.Let’s build.With Jake.We’re going to see that the world is going to be good.So our constructor here must be valid and good.We can clear and run, Rooster.We’re going to see that the Initializer List constructor is called.So this is a step forward.Now that this guy is being called, we need to grab the data from the inside.Let’s see some initial things we can do on this list here.So the first thing we can do is print its size.We can print that out and say list size, and we can say list size.We can call the size method on this.And it is going to give us however many elements we have in our list here that was passed to us as aresult of the call here.Let’s build and run and see the size it should be.Two because we have two elements in our initializer here.So let’s clear and run Rooster.You’re going to see that the list size is two.And if you pass in more than two elements, the initializer list is going to take that.So let’s put in a 5.9, a 6.7 or 8 and a 4.3 here.This is going to do and you’re going to see that this is going to take it even if we have only two membervariables and the compiler trusts that we know what to do with the data that we are taking here becausewe built our own initializer list constructor.So if we print the size, we should see however many elements we have in here and I think it is fiveelements one, two, three, four, five.We should see a five printed out.So let’s build this and we’re going to clear and run Rooster.And you see that we have five elements.We are able to grab our data in our initializer and use that in our constructor here.This is pretty powerful.Okay, So now that we have this, we want to grab the first and second element because that’s what wewant to use to initialize our member variables here.To do this, we will have to use the begin pointer.That’s the list type here has.And to play with this, let’s do this.We’re going to print the first element.We’re going to save this and then we’re going to dereference the begin pointer.So we’re going to dereference and say list and call the begin method and this is going to dereferencethe pointer to the first element in the list here and it’s going to give us the value in there.So what we will get here in this case is 22.4 here, print it out.Let’s put in a new line character and we’re going to print this.Let’s build with GCC and we’re going to see our first element printed out.The build is good so we can run this.Let’s clear and run Rooster.We’re going to see that the first element is this guy here.We can even grab the second element.This is cool, right?So we can do stdcout and we’re going to say second element and we’re going to grab that.So we’re going to dereference the second element and we’re going to use Pointer arithmetic on this pointerhere.So we can say list begin and add one to that pointer.This is going to move us to the next spot.If we do this and print this out, we’re going to grab the second element and that’s going to be 65.4here.Let’s build and see that this is actually the case.So the world is good.We can clear and run.Rooster You see that 65.4 is our second element.We can even go on and keep printing stuff we have in our initializer.So for example, we can use the for loop.Let’s use that so we can do something like for size T and say I and initialize this to zero and we’regoing to keep looping as long as I is less than the size of our list.So we’re going to say I less than list size and then we’re going to increment, we’re going to put inour incrementing part, then we’re going to hit the body and then we’re going to print the element ata given index in this initializer list here.So we’re going to say stdcout and say element at index.We’re going to say the index.Let’s put I here and then we’re going to put a square bracket and we’re going to say which element itis and we’re going to use the pointer arithmetic notation.We’re going to Dereference and say list begin and we’re going to add the current iteration we are at.So we’re going to add I here and I think this is going to work.This is really cool, right?So we’re going to print all the elements that we have in our initializer, but notice that we’re goingto do that using a loop in the body of our Initializer List constructor.This is really cool.You can’t tell how excited I was when I learned about this.Let’s go out and see this working.We’re going to build with GCC.The build is good.We can clear.And run Rooster.We’re going to see that we have our elements in our initializer list here.We have from 22.4 that we have here, all the way to 4.3, which is the last element.And this is completely dynamic.If we change and put in more elements, let’s put in a 5.6 and do a 9.3 and do a 4.3.Let’s build and run and we’re going to see that the list here is going to grow dynamically.Notice that we didn’t need to change a thing and the body of our constructor here, we are able to grabour initializers and use them to do stuff.So let’s build and really show you this.We’re going to use GCC to do this, so let’s clear and run Rooster.We’re going to see that we are grabbing everything here.Eight elements from 0 to 7 and the last one is 4.3 here.This is really cool.So now that we know how to do this, we can use this to our advantage and grab the first element anduse that to initialize our member variable X and use the second element to initialize the second element,which is Y.Here, let’s do that.We’re going to comment all this out because I don’t want to take this out.I think it is really cool.So let’s keep this down in here in case you need to use this as a reference.And we’re just going to grab the first element and store that in our X member variable.So we’re going to dereference the begin pointer.We’re going to say list begin and that’s going to be our first element and we’re going to store thatin X, We’re going to grab the second element and store that in our Y member variable.Let’s do this and we’re going to use pointer arithmetic because I think it is good we’re going to addone because that’s going to be the second element.The element at index one is going to be what we want, which is going to be 65.4 here and we’re goingto use that to initialize our Y member variable.So by this point, if we go down and print information about P one, we’re going to say print point.We should see all the member variables printed out.We’re going to print what we have in X and we’re going to print what we have in Y, and you’re goingto see that it is the two pieces of data we have in the first spot and the second spot in our initializerhere.Let’s build and run.And you’re going to see this.We’re going to use GCC, as always.You see, the build is good.We’re going to clear and run Rooster.You see that?Our point is really working and it is exactly what we saw with the default initializer list that wasgenerated by the compiler for us.But now we have our own initializer list constructor and we can really make it do whatever we want.For example, we can flip all these two things and make X take the second element and we can do this.So if this makes sense for whatever application you are designing, please use this to your advantage.You already know how to do this.So if you look at this, you’re going to see that what we pass in here 22 is going to be used to initializeour Y member variable.So you can really customize how your initializer list constructors work.Let’s take this back to X and Y because I think what we just did is confusing, but hopefully this provesthat you can do all kinds of crazy things in your initializer list.Constructors.Okay.Before I let you go, let’s add some safety mechanism on our point class here and throw a compiler error.If people really pass more than two elements in our initializer list here because we don’t really wantmore.This is a point.And the point is going to have two member variables, and that’s what we want to take from this here.No more, no less.We just want two elements in our initializer here.We can enforce this by using a staticassert.We already know that.So what we can do, I think some of you are going to believe this can work is use a staticassert.But I hate to break to you.This is not going to work as you see here, because the size of our list is the runtime thing.It is something that is known at runtime when somebody initializes your object.So we can’t use this at compile time.It is not a constant expression.What we really need to use is something that works at runtime.So we’re going to use an assert.I don’t think we have seen what an assert is, but it is a mechanism.We have to check for things at runtime and if our check fails, we’re going to throw a compiler error.Let’s do that.We’re going to include a utility which is going to allow us to do that.So we’re going to include C assert and it is a library that is going to give us access to what we areabout to use and what we’re going to do is.A version of the Staticassert here, but it is going to be a runtime thing which is going to be evaluatedwhen your application is running.So we’re going to say assert.We can save that and inside we’re going to put in our assert expression here.So we’re going to basically be checking if the size of the list is two.If it’s not two, we’re going to throw a compiler error and we’re going to try and build our programhere.We’re going to see our compiler error.We’re not going to get a compiler error, but we’re going to get a runtime error when our code startsto run.So I have to correct this.Sorry, I was wrong about this.So we’re going to clear and run Rooster, and if we try to run it, you’re going to see that our programcrashed and it is telling us what is wrong here.It is saying list size equal to.So this has failed and it is going to tell us where this actually failed in our program.So, for example, we can go in our code and see the main CPP file and it’s going to give us the linenumber.And this is really cool.So we can go back there and look in the main CPP file, look at line eight and see what failed and wewill know that we have passed more than what is needed in this initializer here.If we change this to two elements, our code is going to build and do what we want.But note that this assert is going to check for things at runtime.When your application is running, let’s build and run.You’re going to see that this is going to work.If we pass less than two elements, we’re going to fail at runtime.So let’s build again.You’re going to see that the build is good.We can clear and run.Rooster and assertion is going to fail.The list size should be two and we only have one element in and our program is going to basically crashhere at line eight.We should fix this problem in our class.We’re going to look at this assert and we’re going to say if the size of the list is not two, we’renot throwing a compiler error.We are going to terminate the program with an error.Let’s say that I think this is more descriptive for this.So we’re going to see the problem here and we’re going to fix it and put in two elements.Let’s put in 34.9.Why not?And we’re going to build and this is going to work.Now, this is pretty cool.Let’s clear and run, Rooster.We’re going to see that we have our data in here.This is really all we set out to do in this lecture.Hopefully, you know that you can use initializer list constructors to your advantage.We are going to stop here in this lecture.In the next one, we’re going to learn about aggregate initialization.Go ahead and finish up here and and meet me there.
🤖 Assistant
C++ 聚合体的大括号初始化与自定义初始化列表构造函数(Initializer List Constructors for Aggregates)
简介: 本节课我们将深入学习C++中用于初始化聚合体的特殊构造函数——初始化列表构造函数。我们将探讨聚合体的概念,编译器如何默认支持这种初始化,以及如何编写自己的初始化列表构造函数来定制行为并进行运行时检查。
1. 什么是聚合体(Aggregates)?以及为什么需要初始化列表构造函数?
在C++中,聚合体是一种特殊的容器类型,它们能够“聚合”或包含许多小的数据项。你可以把它想象成一个大盒子,里面放着许多小盒子。
聚合体的特性和例子:
- 本质:聚合体是一种将多个独立的数据项聚集在一起的复合类型。
- 具体定义:C++对聚合体有严格的定义,但对于初学者,你可以暂时将其理解为一种可以容纳其他数据的容器。
- 常见例子:
- 统一的初始化语法:对于这些聚合体,C++提供了一种统一且非常简洁的初始化语法,即使用大括号
{}
。- 例如:
Point p = {10.0, 20.0};
或者int arr[] = {1, 2, 3};
。
- 例如:
- 编译器的工作:当你使用这种大括号初始化语法时,编译器会“知道”如何生成必要的工具来将这些数据放入你的容器中。它会根据数据在括号中的顺序,将它们依次存入聚合体的成员中。
为什么是初始化列表构造函数?
当编译器看到你使用大括号 {}
来初始化一个对象时,它实际上会在幕后(behind the scenes)调用或生成一个特殊的构造函数来处理这些数据。这个构造函数就是初始化列表构造函数。
对于像struct Point { double x; double y; };
这样的聚合体,即使你没有显式定义任何构造函数,编译器也会为你生成一个默认的初始化列表构造函数。这个默认行为通常是:按成员声明的顺序,将大括号中的数据依次赋给对应的成员。例如,Point p = {10.0, 20.0};
会将 10.0
赋给 x
,20.0
赋给 y
。
2. 自定义初始化列表构造函数:std::initializer_list
虽然编译器可以为我们生成默认的初始化列表构造函数,但有时我们需要改变这种默认行为,例如:
- 改变初始化顺序:你可能想让第一个传入的值初始化
y
,第二个值初始化x
。 - 添加校验逻辑:你可能希望确保用户传入的数据项数量符合预期。
- 执行自定义操作:你可能需要根据传入的列表执行一些额外的逻辑。
为了实现这些自定义,我们可以显式地定义自己的初始化列表构造函数。
语法结构:
一个自定义的初始化列表构造函数通常长这样:
1 |
|
std::initializer_list<T>
的作用:
- 模板类:
std::initializer_list
是C++标准库中的一个模板类。它用来表示一个不可变(immutable)的同类型元素的序列。 - 类型匹配:
std::initializer_list<double>
中的double
类型应该与你希望它接收的列表元素类型相匹配。如果你的类成员是double
,那么这里通常也用double
。 - 编译器转换:当你使用大括号
{}
初始化你的类对象时(例如MyClass obj = {v1, v2, v3};
),编译器会捕获{v1, v2, v3}
中的所有数据,并用它们来构造一个临时的std::initializer_list<T>
对象。然后,这个std::initializer_list
对象会被作为参数传递给你定义的初始化列表构造函数。
3. 如何访问 std::initializer_list
中的元素?
std::initializer_list
提供了几种方法来访问它所包含的元素:
size()
方法:返回列表中元素的数量。- 例如:
list_param.size()
。
- 例如:
- 迭代器 (
begin()
,end()
): - 解引用运算符
*
:如果你有一个迭代器(或指针),可以使用*
来获取它所指向的实际值。- 例如:
*list_param.begin()
将获取列表中的第一个元素的值。
- 例如:
- 指针算术:虽然
begin()
返回的是迭代器,但对于std::initializer_list
的迭代器,你通常可以像对待指针一样对它们执行加减运算,以访问后续元素。- 例如:
*(list_param.begin() + 1)
将获取列表中的第二个元素的值(索引为1)。
- 例如:
示例代码(如何在构造函数中使用):
1 |
|
遍历 std::initializer_list
中的所有元素:
虽然对于 Point
这样固定数量成员的类,我们通常只取前几个元素,但 std::initializer_list
支持遍历所有元素。
1 |
|
4. QA 闪存卡(QA Flash Cards)
Q1:C++ 中的聚合体是什么?
A1:聚合体是指符合特定条件的类类型,通常是不包含用户声明的构造函数、析构函数、虚函数、私有/保护非静态数据成员、基类等的结构体或类,以及数组类型。它们可以使用大括号初始化语法进行初始化。
Q2:初始化列表构造函数的目的是什么?
A2:它的目的是允许你使用统一的大括号初始化语法 {}
来构造和初始化对象,并为这种初始化提供自定义逻辑,而不仅仅依赖于编译器生成的默认行为。
Q3:如何自定义一个初始化列表构造函数?
A3:通过定义一个接受 std::initializer_list<T>
类型参数的构造函数,其中 T
是你希望列表元素具有的类型。例如:MyClass(std::initializer_list<int> list_param);
。
Q4:如何在 std::initializer_list
中访问元素?
A4:可以使用 list.size()
获取元素数量;使用 *(list.begin())
获取第一个元素;使用 *(list.begin() + N)
获取第 N+1
个元素(指针算术);或者使用基于范围的 for
循环遍历所有元素。
Q5:static_assert
和 assert
有什么区别?
A5:
static_assert
用于编译期检查,它要求其条件是一个常量表达式。如果条件为假,则会导致编译错误。assert
用于运行期检查,它在其条件为假时会终止程序的执行(通常是抛出异常或调用abort()
)。它通常用于调试目的,在发布版本中可以被禁用。
5. 常见误解与错误(Common Misunderstandings or Mistakes)
- 混淆
std::initializer_list
与普通多参数构造函数:- 错误:认为
Point(double x_val, double y_val)
也是一个初始化列表构造函数。 - 澄清:虽然看起来相似,但只有参数类型是
std::initializer_list<T>
的构造函数才是初始化列表构造函数。它允许你使用统一的{}
语法,并且可以接受任意数量的元素(只要类型匹配)。
- 错误:认为
- 试图对
std::initializer_list::size()
使用static_assert
: - 忘记包含必要的头文件:
- 错误:使用
std::initializer_list
但没有包含<initializer_list>
。使用assert
但没有包含<cassert>
。 - 澄清:这些都是标准库组件,必须显式包含对应的头文件。
- 错误:使用
- 未处理
std::initializer_list
中元素数量不匹配的情况: - 认为
std::initializer_list
只能用于聚合体: - 在构造函数体内进行成员初始化:
- 错误:在构造函数体内使用
x = *(list.begin());
- 澄清:虽然可以,但更推荐使用成员初始化列表来初始化成员变量。例如:
Point(std::initializer_list<double> list) : m_x(*(list.begin())), m_y(*(list.begin() + 1)) { ... }
这样可以避免先默认构造再赋值的开销,尤其对于复杂对象。然而,对于std::initializer_list
本身,通常需要在构造函数体内进行处理,因为它是参数。这里特指我们自己的成员变量m_x
,m_y
。
- 错误:在构造函数体内使用
6. 编程练习(Coding Exercise)
现在,请你来完成一个简单的练习。
任务:创建一个名为 Rectangle
的结构体,它代表一个二维矩形。
Rectangle
应该有两个double
类型的私有成员变量:m_width
(宽度) 和m_height
(高度)。- 它应该有一个自定义初始化列表构造函数,接受一个
std::initializer_list<double>
参数。 - 这个构造函数应该:
- 添加一个公共的
print_dimensions()
方法来打印矩形的宽度和高度。 - 在
main
函数中,创建Rectangle
对象:- 成功初始化一个
Rectangle
对象。 - (可选)尝试用不正确的元素数量初始化一个
Rectangle
对象,观察assert
的行为(记得注释掉这行代码,否则程序会终止)。
- 成功初始化一个
请在下面的代码块中填写你的解决方案:
1 |
|
🧑💻 User
1 |
|
🤖 Assistant
好的,你的解决方案已收到!作为你的C++专业导师,我将仔细审查你的代码,提供详细的反馈,并给出一些扩展的思考。
7. 评估用户解决方案并给出实际答案和解释
解决方案评分
总分:5/5
评价: 你的解决方案非常出色!你成功地实现了所有要求,包括定义结构体、实现初始化列表构造函数、使用 assert
进行运行时校验,并通过指针算术正确地初始化了成员变量。代码逻辑清晰,功能实现完整。
具体得分点:
Rectangle
结构体定义与成员变量私有化 (1/1):你正确地将m_width
和m_height
声明为私有成员,这是一个良好的封装实践。- 自定义初始化列表构造函数 (1/1):你成功地定义了接受
std::initializer_list<double>
参数的构造函数,这是本节课的核心内容。 - 打印调用信息 (1/1):构造函数中包含了
std::cout << "Initializer List called." << std::endl;
,符合要求。 - 使用
assert
进行运行时校验 (1/1):你正确地使用了assert(list.size() == 2 && "list size shall be two to include width and length");
来检查列表大小,并给出了有用的错误信息。 - 正确初始化成员变量 (1/1):你使用
m_width = *(list.begin());
和m_height = *(list.begin() + 1);
来初始化成员,精确地利用了指针算术和解引用。 print_dimensions()
方法 (1/1):你实现了正确的打印方法,并且是const
成员函数,表示它不会修改对象状态,这也是一个好习惯。main
函数测试 (1/1):main
函数中的测试用例也演示了正确的初始化。
你唯一没有完全按照提示(“可以使用成员初始化列表来初始化成员变量”)做的地方是:你在构造函数体内对 m_width
和 m_height
进行了赋值,而不是在成员初始化列表中。这并非错误,但涉及到C++的一个重要最佳实践,我们会在下面的“实际答案和解释”中详细说明。
实际答案和解释
你的代码是完全正确的,并且能够正常工作。这里我提供一个稍作优化的版本,主要是在构造函数中使用了成员初始化列表。
优化的代码:
1 |
|
为什么推荐使用成员初始化列表?
在C++中,构造函数有两种方式来为成员变量赋值:
在构造函数体内赋值 (你目前的做法):
cpp1
2
3
4
5Rectangle(std::initializer_list<double> list) {
// ...
m_width = *(list.begin());
m_height = *(list.begin() + 1);
}这种方式的问题在于,对于非基本类型(如自定义类对象),在进入构造函数体之前,它们的默认构造函数会先被隐式调用(如果存在)。然后,在构造函数体内,你再对它们进行一次赋值操作。这意味着可能会有一次不必要的默认构造和随后的赋值开销。
使用成员初始化列表 (推荐做法):
cpp1
2
3
4
5
6Rectangle(std::initializer_list<double> list)
: m_width(*(list.begin())),
m_height(*(list.begin() + 1))
{
// ... 构造函数体
}这种方式直接在成员变量被创建时就使用指定的值进行初始化。这通常效率更高,因为它避免了先进行默认构造再进行赋值的步骤。对于某些没有默认构造函数的类类型成员,或者
const
成员、引用成员,你必须使用成员初始化列表来初始化它们。
虽然对于像 double
这样的基本类型,这种差异通常可以忽略不计,但养成使用成员初始化列表的好习惯对编写高效和正确的C++代码非常重要。你的代码的执行顺序是:
list.begin()
和list.begin() + 1
被求值。m_width
被初始化为第一个值。m_height
被初始化为第二个值。- 构造函数体内的
std::cout
语句执行。 assert
语句执行。
这确保了成员变量在构造函数体开始执行之前就已经被初始化了。
8. 扩展问题和建议(Extended Questions and Suggestions)
你已经很好地掌握了初始化列表构造函数的基本概念和用法。接下来,你可以探索以下几个方面,加深对相关知识的理解:
成员初始化列表的深入理解:
std::initializer_list
的其他应用:- 容器的初始化:许多标准库容器,如
std::vector
,std::map
,std::set
等,都提供了接受std::initializer_list
的构造函数。你可以尝试用这种方式初始化一个std::vector<double>
。 - 可变参数函数:思考如何设计一个函数,它能接受任意数量的相同类型参数,而不仅仅是固定两个。
std::initializer_list
是实现这种“可变参数”的一种安全且类型安全的方式。 - 设计通用容器:如果让你设计一个简单的动态数组类(类似
std::vector
),如何利用std::initializer_list
来实现它的便捷初始化?这需要你处理列表中元素的复制和存储,并考虑内存管理。
- 容器的初始化:许多标准库容器,如
运行时检查与异常处理:
聚合体的详细定义:
继续保持这种学习热情,你会在C++的学习道路上取得更大进步!